﻿
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace CodingMuscles.Text.Regexp
{
    /// <summary>
    /// Builds and stores a regular expression pattern as a <see cref="string"/>
    /// </summary>
    public class RegexPatternBuilder : IRegexPatternBuilder
    {
        /// <see cref="IRegexPatternBuilder.Match(Anchor)"/>
        public IRegexPatternBuilder Match(Anchor anchor)
        {
            switch (anchor)
            {
                case Anchor.EndOfLine:
                    Pattern += "$";
                    break;
                case Anchor.EndOfString:
                    Pattern += "\\z";
                    break;
                case Anchor.EndOfStringAndNewline:
                    Pattern += "\\Z";
                    break;
                case Anchor.NonWordBoundary:
                    Pattern += "\\B";
                    break;
                case Anchor.PreviousMatch:
                    Pattern += "\\G";
                    break;
                case Anchor.StartOfLine:
                    Pattern += "^";
                    break;
                case Anchor.StartOfString:
                    Pattern += "\\A";
                    break;
                case Anchor.WordBoundary:
                    Pattern += "\\b";
                    break;
                default:
                    throw new NotSupportedException();
            }

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Comment"/>
        public IRegexPatternBuilder Comment(string comment)
        {
            Pattern += $"(?#{comment})";
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(string)"/>
        public IRegexPatternBuilder Match(string match)
        {
            Pattern += Escape(match);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(ushort)"/>
        public IRegexPatternBuilder Match(ushort match)
        {
            Pattern += ToString(match);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(ushort, int, Quantifier)"/>
        public IRegexPatternBuilder Match(ushort match, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            Pattern += ToString(match) + ToString(instances, quantifier);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(ushort, int, int)"/>
        public IRegexPatternBuilder Match(ushort match, int minInstances, int maxInstances)
        {
            Pattern += ToString(match) + ToString(minInstances, maxInstances);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(CharacterClass)"/>
        public IRegexPatternBuilder Match(CharacterClass @class)
        {
            Pattern += ToString(@class);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(CharacterClass, int, Quantifier)"/>
        public IRegexPatternBuilder Match(CharacterClass @class, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            Pattern += ToString(@class) + ToString(instances, quantifier);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(CharacterClass, int, int)"/>
        public IRegexPatternBuilder Match(CharacterClass @class, int minInstances, int maxInstances)
        {
            Pattern += ToString(@class) + ToString(minInstances, maxInstances);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(char)"/>
        public IRegexPatternBuilder Match(char match)
        {
            Pattern += Escape(match);
            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(char, int, Quantifier)"/>
        public IRegexPatternBuilder Match(char match, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            Pattern += Escape(match);
            Pattern += ToString(instances, quantifier);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(char, int, int)   "/>
        public IRegexPatternBuilder Match(char match, int minInstances, int maxInstances)
        {
            Pattern += Escape(match);
            Pattern += ToString(minInstances, maxInstances);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(Action{ICharacterSetPatternBuilder})"/>
        public IRegexPatternBuilder Match(Action<ICharacterSetPatternBuilder> builder)
        {
            var set = new CharacterSetPatternBuilder();
            builder(set);

            Pattern += set.ToString();

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(Action{ICharacterSetPatternBuilder}, int, Quantifier)"/>
        public IRegexPatternBuilder Match(Action<ICharacterSetPatternBuilder> builder, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            var set = new CharacterSetPatternBuilder();
            builder(set);

            Pattern += set + ToString(instances, quantifier);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Match(Action{ICharacterSetPatternBuilder}, int, int)"/>
        public IRegexPatternBuilder Match(Action<ICharacterSetPatternBuilder> builder, int minInstances, int maxInstances)
        {
            var set = new CharacterSetPatternBuilder();
            builder(set);

            Pattern += set + ToString(minInstances, maxInstances);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.NotMatch(Action{ICharacterSetPatternBuilder})"/>
        public IRegexPatternBuilder NotMatch(Action<ICharacterSetPatternBuilder> builder)
        {
            var set = new CharacterSetPatternBuilder(true);
            builder(set);

            Pattern += set.ToString();

            return this;
        }

        /// <see cref="IRegexPatternBuilder.NotMatch(Action{ICharacterSetPatternBuilder}, int, Quantifier)"/>
        public IRegexPatternBuilder NotMatch(Action<ICharacterSetPatternBuilder> builder, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            var set = new CharacterSetPatternBuilder(true);
            builder(set);

            Pattern += set + ToString(instances, quantifier);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.NotMatch(Action{ICharacterSetPatternBuilder}, int, int)"/>
        public IRegexPatternBuilder NotMatch(Action<ICharacterSetPatternBuilder> builder, int minInstances, int maxInstances)
        {
            var set = new CharacterSetPatternBuilder(true);
            builder(set);

            Pattern += set + ToString(minInstances, maxInstances);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, bool)"/>
        public IRegexPatternBuilder Group(Action<IRegexPatternBuilder> builder, bool capture)
        {
            return Enclose(capture ? "(" : "(?:", () => builder(this), ")");
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, int, Quantifier, bool)"/>
        public IRegexPatternBuilder Group(Action<IRegexPatternBuilder> builder, int instances, Quantifier quantifier, bool capture)
        {
            return Enclose(capture ? "(" : "(?:", () => builder(this), ")" + ToString(instances, quantifier));
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, int, int, bool)"/>
        public IRegexPatternBuilder Group(Action<IRegexPatternBuilder> builder, int minInstances, int maxInstances, bool capture)
        {
            return Enclose(capture ? "(" : "(?:", () => builder(this), ")" + ToString(minInstances, maxInstances));
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder})"/>
        public IRegexPatternBuilder Group(Action<IRegexPatternBuilder> builder)
        {
            return Enclose("(", () => builder(this), ")");
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, int, Quantifier)"/>
        public IRegexPatternBuilder Group(Action<IRegexPatternBuilder> builder, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            Group(builder);
            Pattern += ToString(instances, quantifier);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, int, int)"/>
        public IRegexPatternBuilder Group(Action<IRegexPatternBuilder> builder, int minInstances, int maxInstances)
        {
            return Enclose("(", () => builder(this), ")" + ToString(minInstances, maxInstances));
        }

        /// <see cref="IRegexPatternBuilder.Group(string, Action{IRegexPatternBuilder})"/>
        public IRegexPatternBuilder Group(string name, Action<IRegexPatternBuilder> builder)
        {
            return Enclose($"(?<{name}>", () => builder(this), ")");
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, int, Quantifier, bool)"/>
        public IRegexPatternBuilder Group(string name, Action<IRegexPatternBuilder> builder, int instances, Quantifier quantifier = Quantifier.Exactly)
        {
            Group(name, builder);
            Pattern += ToString(instances, quantifier);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Group(Action{IRegexPatternBuilder}, int, int)"/>
        public IRegexPatternBuilder Group(string name, Action<IRegexPatternBuilder> builder, int minInstances, int maxInstances)
        {
            Group(name, builder);
            Pattern += ToString(minInstances, maxInstances);

            return this;
        }

        /// <see cref="IRegexPatternBuilder.Alternate"/>
        public IRegexPatternBuilder Alternate(Action<IRegexPatternBuilder> leftHandBuilder, Action<IRegexPatternBuilder> rightHandBuilder)
        {
            leftHandBuilder(this);
            Pattern += "|";
            rightHandBuilder(this);

            return this;
        }

        /// <see cref="object.ToString"/>
        public override string ToString()
        {
            return Pattern;
        }

        /// <summary>
        /// The accumulated pattern
        /// </summary>
        protected string Pattern = string.Empty;

        /// <summary>
        /// Encloses the pattern created by invoking <paramref name="middle"/> between two strings
        /// </summary>
        /// <param name="start">The start string</param>
        /// <param name="middle">Delegate that updates the pattern</param>
        /// <param name="end">The end string</param>
        /// <returns>A reference to this object after it is modified</returns>
        private IRegexPatternBuilder Enclose(string start, Action middle, string end)
        {
            Pattern += start;
            middle();
            Pattern += end;

            return this;
        }

        /// <summary>
        /// Escapes a character if it requires it
        /// </summary>
        /// <param name="c">The character to escape</param>
        /// <param name="inCharacterSet">True if the character appears in a character set</param>
        /// <param name="first">If the character appears in the first position of a character set</param>
        /// <param name="last">If the character appears in the last position of a character set</param>
        /// <param name="negatePresent">True if a negate character is present; false if not</param>
        /// <returns>The character as a string, escaped if required</returns>
        private static string Escape(char c, bool inCharacterSet = false, bool first = false, bool last = false, bool negatePresent = false)
        {
            switch (c)
            {
                case '\n':
                    return @"\n";
                case '\r':
                    return @"\r";
                case '\b':
                    return @"\b";
                case '\a':
                    return @"\a";
                case '\t':
                    return @"\t";
                case '\v':
                    return @"\v";
                case '\f':
                    return @"\f";
            }

            var requiresEscape = false;
            switch (c)
            {
                case '[':
                case ']':
                case '{':
                case '}':
                case '\\':
                    requiresEscape = true;
                    break;
                case '-':
                    requiresEscape = inCharacterSet && !(first || last);
                    break;
                case '^':
                    requiresEscape = !inCharacterSet || (first && !negatePresent);
                    break;
                case '+':
                case '$':
                case '(':
                case ')':
                case '|':
                case '*':
                    requiresEscape = !inCharacterSet;
                    break;
            }

            return requiresEscape ? @"\" + c : c.ToString();
        }

        /// <summary>
        /// Creates a modified string where special characters have been properly escaped 
        /// for use in a regular expression
        /// </summary>
        /// <param name="s">The initial string</param>
        /// <returns>The escaped string</returns>
        private static string Escape(string s)
        {
            return s.Aggregate(new StringBuilder(s.Length), (b, c) =>
            {
                b.Append(Escape(c));
                return b;
            }).ToString();
        }

        /// <summary>
        /// Converts a quantifier into a string
        /// </summary>
        /// <param name="instances">The number of instances in the quantifier</param>
        /// <param name="quantifier">How to interpret the meaning of the number of instances</param>
        /// <returns>A formatting quantifier string</returns>
        private static string ToString(int instances, Quantifier quantifier)
        {
            if (instances == 0 && quantifier == Quantifier.OrMore)
                return "*";

            if (instances == 1 && quantifier == Quantifier.OrMore)
                return "+";

            return $"{{{instances}{(quantifier == Quantifier.OrMore ? "," : string.Empty)}}}";
        }

        /// <summary>
        /// Converts a quantifier into a string
        /// </summary>
        /// <param name="minInstances">The minimum number of instances, inclusive</param>
        /// <param name="maxInstances">The maximum number of instances, inclusive</param>
        /// <returns>A formatting quantifier string</returns>
        private static string ToString(int minInstances, int maxInstances)
        {
            if (minInstances > maxInstances)
                throw new ArgumentOutOfRangeException(nameof(maxInstances));

            if (minInstances == 0 && maxInstances == 1)
                return "?";

            if (minInstances == maxInstances)
                return ToString(minInstances, Quantifier.Exactly);

            return $"{{{minInstances},{maxInstances}}}";
        }

        /// <summary>
        /// Converts a character class to its string equivalent
        /// </summary>
        /// <param name="class">The character class to convert</param>
        /// <returns>A formatted string containing the class</returns>
        private static string ToString(CharacterClass @class)
        {
            switch (@class)
            {
                case CharacterClass.Word:
                    return @"\w";
                case CharacterClass.Whitespace:
                    return @"\s";
                case CharacterClass.Any:
                    return @".";
                case CharacterClass.Digit:
                    return @"\d";
                case CharacterClass.NonDigit:
                    return @"\D";
                case CharacterClass.NonWhitespace:
                    return @"\S";
                case CharacterClass.NonWord:
                    return @"\W";
            }

            throw new NotSupportedException();
        }

        /// <summary>
        /// Converts a numeric value to a regular expression pattern
        /// </summary>
        /// <param name="value">The value to convert</param>
        /// <returns>A formatting string containing the numeric value</returns>
        private static string ToString(ushort value)
        {
            return value <= 255 ? $"\\x{value:X2}" : $"\\u{value:X4}";
        }
        
        class CharacterSetPatternBuilder : ICharacterSetPatternBuilder
        {
            public CharacterSetPatternBuilder(bool negate = false)
            {
                _negate = negate;
            }

            public ICharacterSetPatternBuilder Include(char min, char max)
            {
                if (min > max)
                    throw new ArgumentException($"Range {min}-{max} in reverse order");

                if (min == max)
                    _set += min;
                else
                    _set += min + "-" + max;

                return this;
            }

            public ICharacterSetPatternBuilder Include(ushort value)
            {
                _set += value <= 255 ? $"\\x{value:X2}" : $"\\u{value:X4}";
                return this;
            }

            public ICharacterSetPatternBuilder Include(char c)
            {
                _literalRanges.Add(new Tuple<int, int>(_set.Length, _set.Length + 1));
                _set += c;
                return this;
            }

            public ICharacterSetPatternBuilder Include(string s)
            {
                _literalRanges.Add(new Tuple<int, int>(_set.Length, _set.Length + s.Length));
                _set += s;
                return this;
            }

            public ICharacterSetPatternBuilder Include(CharacterClass @class)
            {
                _set += RegexPatternBuilder.ToString(@class);
                return this;
            }

            public ICharacterSetPatternBuilder IncludeUnicodeCategory(string categoryName)
            {
                _set += $"\\p{{{categoryName}}}";
                return this;
            }

            public ICharacterSetPatternBuilder ExcludeUnicodeCategory(string categoryName)
            {
                _set += $"\\P{{{categoryName}}}";
                return this;
            }

            public override string ToString()
            {
                var result = _negate ? "[^" : "[";
                var first = true;

                for (var c = 0; c < _set.Length; c++, first = false)
                {
                    // only escape literal values added explicitly by the caller
                    if (_literalRanges.Any(r => c >= r.Item1 && c < r.Item2))
                        result += Escape(_set[c], true, first, c == _set.Length - 1, _negate);
                    else
                        result += _set[c];
                }

                return result + "]";
            }

            private string _set = string.Empty;
            private readonly List<Tuple<int, int>> _literalRanges = new List<Tuple<int, int>>();
            private readonly bool _negate;
        }
    }
}
